﻿using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using BLToolkit.Data;

using NRails.Migrations;
using Nemerle;

namespace NRails.Macros.Migration
{
  public macro insert(table, values) : PExpr
  syntax("insert", "into", table, "values",  values)
  {
      SqlImpl.Insert(Nemerle.Macros.ImplicitCTX(), table, values);
  }

  public macro @sqlreader(body, params args : array[expr]) : PExpr
  syntax("select", "(", args, ")", body)
  {
      SqlImpl.SqlReader(args, body);
  }
  
  public macro @sql(var, params args : array[expr]) : PExpr
  syntax("sql", "(", args, ")", Optional("returning", var))
  {
      SqlImpl.Sql(args, var);
  }
  
  module SqlImpl
  {
      paramRegex : Regex = Regex(@"\$\(?([\w\d]+\s*:\s*[\w\d\[\]]+|[\w\d]+)\)?");
      public SqlReader(args : array[PExpr], body : PExpr) : PExpr
      {
          def (db, sqlStr, args) = matchParams("select", args);
          def rxMatch = paramRegex.Matches(sqlStr);
          def parms = rxMatch.Cast.[Match]().Map(m => 
          {
              regexp match (m.Groups[1].Value)
              {
                | @"(?<name>\w[\w\d]*)" => (name, "string");
                | @"(?<name>\w[\w\d]*)\s*:\s*(?<type_>array\s*\[[\w\d]*\])" => (name, type_);
                | @"(?<name>\w[\w\d]*)\s*:\s*(?<type_>\w[\w\d]*)" => (name, type_);
                | _ => Message.FatalError($"Invalid var $(m.Groups[1].Value)");
              }
          });
          def tempQuery = paramRegex.Replace(sqlStr, m => m.Groups[1].Value.Split(':')[0].Trim());
          def (q, t) = parseSql(tempQuery, args.ToArray());

          def getReaderMethod(type_, number)
          {
              regexp match (type_)
              {
                  | "string" => <[ reader.GetString($(number : int)) ]>
                  | "char" => <[ reader.GetString($(number : int))[0] ]>
                  | "bool"
                  | "boolean" => <[ reader.GetBoolean($(number : int)) ]>
                  | "guid" => <[ reader.GetGuid($(number : int)) ]>
                  | "float" => <[ reader.GetFloat($(number : int)) ]>
                  | "double" => <[ reader.GetDouble($(number : int)) ]>
                  | "int16" => <[ reader.GetInt16($(number : int)) ]>
                  | "int"
                  | "int32" => <[ reader.GetInt32($(number : int)) ]>
                  | "int64" => <[ reader.GetInt64($(number : int)) ]>
                  | "datetime" => <[ reader.GetDateTime($(number : int)) ]>
                  | "object" => <[ reader.GetValue($(number : int)) ]>
                  | @"array\[(?<subtype>[\w\d]*)\]" =>
                    match (subtype)
                    {
                        | "byte" => <[ {
                                def len = reader.GetBytes($(number : int), 0, null, 0, 0) :> int;
                                def result = array(len);
                                _ = reader.GetBytes($(number : int), 0, result, 0, len);
                                result;
                            }]>
                        | "char" => <[ {
                                def len = reader.GetChars($(number : int), 0, null, 0, 0);
                                def result = array(len);
                                _ = reader.GetChars($(number : int), 0, result, 0, len);
                                result;
                            }]>
                        | _ => <[ reader.GetValue($(number : int)) :> array[$(subtype : dyn)] ]>
                    }
                  | _ => Message.FatalError($"Unknown type $type_");
              }
          }

          def cmd = prepareCmd(db, q, t);
          mutable i = -1;
          def vars = parms.Map((name, type_) => {
              i++;
              def readerMethod = getReaderMethod(type_, i);
              <[ def $(name : usesite) = $readerMethod; ]>
          });
          
          def concat(vars, body)
          {
            match (vars)
            {
               | x :: tail => x :: concat(tail, body);
               | [] => [body]
            }
          }
          
          def newBody = concat(vars, body);
          <[
            { $cmd }
            def reader = $db.ExecuteReader();
            try
            {
                while (reader.Read())
                {
                    ..$newBody
                }
            }
            finally
            {
                reader.Dispose();
            }
          ]>
      }

      public Sql(args : array[PExpr], returningVar : PExpr) : PExpr
      {
          def (db, sqlStr, args) = matchParams("sql", args);
          def (sql, paramNames) = parseSql(sqlStr, args.ToArray());
          if (args.Length > paramNames.Count)
          {
              Message.FatalError(args.Last().Location, "Get enough parameters in query");
          }
          else
          {
              def cmd = prepareCmd(db, sql, paramNames);

              match (returningVar)
              {
                  | PExpr.Ref(_) =>
                      <[
                         def $returningVar = {
                             { $cmd }
                             $db.ExecuteNonQuery();
                         }
                      ]>
                  | _ =>
                      <[
                         { $cmd }
                         _ = $db.ExecuteNonQuery();
                      ]>
              }
          }
      }

      private matchParams(name : string, args : array[PExpr]) : (PExpr * string * list[PExpr])
      {
          def (db, sql, args) = match (args.ToNList())
          {
              | db :: sql :: tail => (db, sql, tail)
              | _ => Message.FatalError($"syntax: $name(db : DbManager, \"select clause\" [, parameter list])");
          }
          def sqlStr = match (sql)
          {
              | PExpr.Literal(Literal.String(t)) => t;
              | _ => Message.FatalError(sql.Location, $"Must be string, got $(sql.GetType())");
          }
          (db, sqlStr, args)
      }
      
      private prepareCmd(db : PExpr, sql : StringBuilder, paramNames : List[(int * PExpr)]) : PExpr
      {
          def paramPositions = paramNames.Map((p, _) => p);
              
          def compilePositions(paramPositions)
          {
              match (paramPositions)
              {
                  | x :: tail => 
                    def pName = $"p$x";
                    <[$db.DataProvider.Convert($(pName : string), BLToolkit.Data.DataProvider.ConvertType.NameToCommandParameter)]> :: compilePositions(tail);
                  | [] => []
              }
          }
          
          def cc = <[$(sql.ToString() : string)]> :: compilePositions(paramPositions);
          def parms = <[ $("sqlStr" : usesite) ]> :: paramNames.Map((pos, value) => 
          {
             def pName = $"p$pos";
             <[ $db.Parameter($(pName : string), $value) ]> 
          });

          <[
             def $("sqlStr": usesite) = System.String.Format(..$cc);
             _ = $db.SetCommand(..$parms);
          ]>
      }
      
      private parseSql(sql : string, args : array[PExpr]) : (StringBuilder * List[(int * PExpr)])
      {
          def sb = StringBuilder();
          def paramNames = List();
          mutable pos = 0;

          def parse(str, skip = false)
          {
              match (str)
              {
                  | ch :: ss => 
                    match (ch)
                    {
                        | '\'' => { _ = sb.Append(ch); parse(ss, !skip); };
                        | '?' when skip => { _ = sb.Append(ch);  parse(ss, skip); }
                        | '?' when !skip => 
                            {
                                _ = sb.Append($"{$pos}");
                                if (args.Length <= pos)
                                    Message.Error("Get enough args");
                                else
                                    paramNames.Add((pos, args[pos]));
                                pos++;
                                parse(ss, skip);
                            };
                            | _ => { _ = sb.Append(ch); parse(ss, skip); };
                    }
                    | _ => (sb, paramNames);
              }
          }
          
          parse(NList.ToList(sql));
      }
      
      public Insert(_ : Typer, table : PExpr, values : PExpr) : PExpr
      {
          mutable nameSeq = 0;
          def genParamName()
          {
              nameSeq++;
              $"@p$nameSeq";
          }
          def valParse(values)
          {
              match (values)
              {
                  | PExpr.Tuple(paramList) => 
                    paramList.Map(fun(p)
                      {
                        def name = genParamName();
                        (<[ db.Parameter($(name : string), $p) ]>, name)
                      });
                  | _ => Message.Error(values.Location, "Invalid values syntax"); []
              }
          }
          def parsedValues = valParse(values);
          def paramNames = parsedValues.Map((_, n) => n);
          def sql = $"insert into $(table.ToString()) values (..$paramNames)";
          
          def setCommandArgs = <[ $(sql : string) ]> :: parsedValues.Map((p, _) => p);
          
          <[
             _ = db.SetCommand(..$setCommandArgs);
             _ = db.ExecuteNonQuery();
          ]>
      }
  }
}
